﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Diagnostics;
using Engine.Dtd.Schema;
using System.Xml;

namespace Engine.Dtd {
    [DebuggerDisplay("CurrentChar = {CurrentChar} LineNumber = {LineNumber} LinePosition = {LinePosition}")]
    internal sealed class EntityReader {
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private StringBuilder builder;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Stack<EntityContext> contextBuffer;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Entity entity;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private EntityResolver entityResolver;

        public EntityReader() {
            this.builder = new StringBuilder();
            this.entityResolver = new EntityResolver();
            }

        public EntityReader(Entity entity)
            : this() {
            Initialize(entity);
            }

        internal void Initialize(Entity entity) {
            Initialize(entity, 1, 0);
            }

        internal void Initialize(Entity entity, int lineNumber, int linePosition) {
            this.entity = entity;
            this.builder.Clear();
            LineNumber = lineNumber;
            LinePosition = linePosition;
            }

        public Entity Entity {
            get { return entity; }
            set {
                if (value == null)
                    throw new ArgumentNullException("value");
                if (value.Stream != null) {
                    if (contextBuffer == null)
                        contextBuffer = new Stack<EntityContext>();
                    if (entity.Parent == value) {
                        EntityContext context = contextBuffer.Pop();
                        currentChar = context.CurrentChar;
                        LineNumber = context.LineNumber;
                        LinePosition = context.LinePosition;
                        }
                    else {
                        contextBuffer.Push(new EntityContext {
                            CurrentChar = currentChar,
                            LineNumber = LineNumber,
                            LinePosition = LinePosition
                        });
                        LineNumber = 1;
                        LinePosition = 0;
                        }
                    }
                entity = value;
                }
            }

        /// <summary>
        /// The line on which this entity was defined.
        /// </summary>
        public int LineNumber { get; private set; }

        /// <summary>
        /// The index into the line where this entity is defined.
        /// </summary>
        public int LinePosition { get; private set; }

        public bool EndOfFile {
            get { return CurrentChar == Characters.EndOfFile; }
            }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private char currentChar;
        /// <summary>
        /// Gets the current character readed from the stream.
        /// </summary>
        public char CurrentChar {
            get { return currentChar; }
            }

        /// <summary>
        /// Reads the next character from the stream.
        /// </summary>
        /// <returns>The next character from the stream.</returns>
        public char ReadChar() {
            currentChar = (char)Entity.Stream.Read();
            while (!XmlConvert.IsXmlChar(currentChar) && currentChar != Characters.EndOfFile) {
                currentChar = (char)Entity.Stream.Read();
                }
            if (currentChar == Characters.LineFeed) {
                LinePosition = 0;
                LineNumber++;
                }
            else
                LinePosition++;
            return currentChar;
            }

        public char PeekChar() {
            return (char)Entity.Stream.Peek();
            }

        /// <summary>
        /// Returns the next character after any whitespace.
        /// </summary>
        /// <returns>The next character that is not whitespace.</returns>
        public char SkipWhitespace() {
            while (XmlConvert.IsWhitespaceChar(currentChar)) {
                ReadChar();
                }
            return currentChar;
            }

        public string ReadToken() {
            builder.Clear();
            while (XmlConvert.IsNCNameChar(currentChar)) {
                builder.Append(currentChar);
                ReadChar();
                }
            return builder.ToString();
            }

        public string ReadNameToken() {
            if (XmlConvert.IsStartNCNameChar(currentChar)) {
                builder.Clear();
                builder.Append(currentChar);
                while (XmlConvert.IsNCNameChar(ReadChar())) {
                    builder.Append(currentChar);
                    }
                return builder.ToString();
                }
            throw new DtdException(String.Format(CultureInfo.CurrentUICulture, "Illegal name start character '{0}'", currentChar), Context());
            }

        public string ReadPrefixNameToken() {
            if (XmlConvert.IsStartNCNameChar(currentChar)) {
                builder.Clear();
                builder.Append(currentChar);
                while (XmlConvert.IsNCNameChar(ReadChar())) {
                    builder.Append(currentChar);
                    }
                if (currentChar == Characters.Colon) {
                    builder.Append(currentChar);
                    if (XmlConvert.IsStartNCNameChar(ReadChar())) {
                        builder.Append(currentChar);
                        while (XmlConvert.IsNCNameChar(ReadChar())) {
                            builder.Append(currentChar);
                            }
                        }
                    }
                return builder.ToString();
                }
            throw new DtdException(String.Format(CultureInfo.CurrentUICulture, "Illegal name start character '{0}'", currentChar), Context());
            }

        /// <summary>
        /// Read a literal from the input stream.
        /// </summary>
        /// <param name="quote">The delimiter for the literal.</param>
        /// <returns>The literal scanned from the input stream.</returns>
        public string ReadLiteral(char quote) {
            builder.Clear();
            ReadChar();
            while (currentChar != Characters.EndOfFile && currentChar != quote) {
                if (currentChar == Characters.Ampersand) {
                    ReadChar();
                    if (currentChar == Characters.Hash) {
                        char entity;
                        if (entityResolver.ResolveCharacterReference(this, out entity)) {
                            builder.Append(entity);
                            continue;
                            }
                        else
                            throw new DtdException("Could not resolve the character reference.", Context());
                        }
                    else {
                        builder.Append(Characters.Ampersand);
                        builder.Append(currentChar);
                        }
                    }
                else
                    builder.Append(currentChar);
                ReadChar();
                }
            // Consume end quote.
            ReadChar();
            return builder.ToString();
            }

        /// <summary>
        /// Reads input until the end of the input stream or until a string of terminator characters is found.
        /// </summary>
        /// <param name="type">The type of the element being read (only used in reporting errors).</param>
        /// <param name="sequence">The string of terminator characters to look for.</param>
        /// <returns>The string read from the input stream.</returns>
        public string ReadToEnd(string type, string sequence) {
            Guard.ArgumentNotNull(type, "type");
            Guard.ArgumentNotNull(sequence, "sequence");
            if (sequence.Length == 0)
                return string.Empty;

            // This method scans over a chunk of text looking for the
            // termination sequence specified by the 'terminators' parameter.

            builder.Clear();
            int startLineNumber = LineNumber;
            char terminator = sequence[0];

            while (ReadChar() != Characters.EndOfFile) {
                if (currentChar == terminator) {
                    int terminatorIndex = 1;
                    int startIndex = builder.Length;
                    while (terminatorIndex < sequence.Length && sequence[terminatorIndex] == PeekChar()) {
                        terminatorIndex++;
                        builder.Append(ReadChar());
                        }
                    if (terminatorIndex == sequence.Length) {
                        builder.Remove(startIndex, sequence.Length - 1);
                        break;
                        }
                    else
                        builder.Append(currentChar);
                    }
                else
                    builder.Append(currentChar);
                }
            if (currentChar == Characters.EndOfFile)
                throw new DtdException(string.Format("{0} starting on line {1} was never closed. {2}", type, startLineNumber), Context());
            // Consume last char in termination sequence.
            ReadChar();
            return builder.ToString();
            }

        public string ReadToEnd() {
            builder.Clear();
            while (ReadChar() != Characters.EndOfFile) {
                builder.Append(currentChar);
                }
            return builder.ToString();
            }

        /// <summary>
        /// Returns a string giving information on how the entity is referenced and declared, walking up the parents until the top level parent entity is found.
        /// </summary>
        /// <returns>Contextual information for the entity.</returns>
        public string Context() {
            if (Entity == null)
                return null;
            StringBuilder builder = new StringBuilder();
            Entity entity = Entity;
            if (entity.IsInternal)
                builder.AppendFormat(CultureInfo.InvariantCulture, "Referenced on line {0}, position {1} of internal entity '{2}'", LineNumber, LinePosition, entity.Name);
            else
                builder.AppendFormat(CultureInfo.InvariantCulture, "Referenced on line {0}, position {1} of '{2}' entity at [{3}]", LineNumber, LinePosition, entity.Name, entity.Url.AbsoluteUri);

            if (contextBuffer != null && contextBuffer.Count > 0) {
                foreach (EntityContext context in contextBuffer) {
                    entity = entity.Parent;
                    if (entity == null)
                        break;
                    builder.AppendLine();
                    if (entity.IsInternal)
                        builder.AppendFormat(CultureInfo.InvariantCulture, "Referenced on line {0}, position {1} of internal entity '{2}'", context.LineNumber, context.LinePosition, entity.Name);
                    else
                        builder.AppendFormat(CultureInfo.InvariantCulture, "Referenced on line {0}, position {1} of '{2}' entity at [{3}]", context.LineNumber, context.LinePosition, entity.Name, entity.Url.AbsoluteUri);
                    }
                }
            return builder.ToString();
            }
        }
    }
